package com.sunday.common.validation;

import com.google.gson.JsonObject;
import com.sunday.common.core.enums.ErrorCodeEnum;
import com.sunday.common.core.exception.BusinessException;
import com.sunday.common.core.validation.annotation.ErrorCodeAttribute;
import com.sunday.common.core.validation.constants.ErrorCodeConstants;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.constraints.*;
import jakarta.validation.groups.Default;
import jakarta.validation.metadata.ConstraintDescriptor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl;
import org.springframework.core.annotation.AnnotatedElementUtils;

import java.lang.annotation.ElementType;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * 参数验证器
 *
 * @author zsy
 * @since 2022/10/15
 */
@Slf4j
public class ParameterValidator {

    /**
     * 验证
     */
    private static Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
    /**
     * 注解枚举映射
     */
    private static final Map<String, ErrorCodeEnum> ANNOTATION_ENUM_MAPPING = new HashMap() {
        {
            /** =========== A0_421("A0421","参数格式不匹配") =============*/

            /**
             * 被注释的元素必须为false。
             * 支持的类型是{@code boolean}和{@code Boolean}。
             * {@code null} 元素认为是有效的。
             */
            put(AssertFalse.class.getName(), ErrorCodeEnum.A0_421);
            /**
             * 被注释的元素必须为true。
             * 支持的类型是{@code boolean}和{@code Boolean}。
             * {@code null} 元素认为是有效的。
             */
            put(AssertTrue.class.getName(), ErrorCodeEnum.A0_421);
            /**
             * 注释的元素必须是{@code null}。
             * 接受任何类型。
             */
            put(Null.class.getName(), ErrorCodeEnum.A0_421);
            /**
             * 带注释的{@code CharSequence}必须匹配指定的正则表达式。
             * 例如 @Pattern(regexp = "^\\s*\\w+(?:\\.{0,1}[\\w-]+)*@[a-zA-Z0-9]+(?:[-.][a-zA-Z0-9]+)*\\.[a-zA-Z]+\\s*$") 邮箱
             * 正则表达式遵循Java正则表达式约定
             * 看到{@link java.util.regex.Pattern}。
             * 接受{@code CharSequence进行}。 {@code null}元素被认为有效。
             */
            put(Pattern.class.getName(), ErrorCodeEnum.A0_421);

            /** =========== A0_410("A0410","请求必填参数为空") =============*/

            /**
             * 注释的元素不能是{@code null}。
             * 接受任何类型。
             */
            put(NotNull.class.getName(), ErrorCodeEnum.A0_410);
            /**
             * 注释的元素不能是{@code null}，并且必须包含至少一个
             * 非空字符。 接受{@code CharSequence}。
             */
            put(NotBlank.class.getName(), ErrorCodeEnum.A0_410);
            /**
             * 注释元素不能为{@code null}或为空。
             * 支持类型:
             * {@code CharSequence}(计算字符序列的长度)
             * {@code Collection}(计算集合大小)
             * {@code Map}(映射大小计算)
             * 数组(数组长度求值)
             */
            put(NotEmpty.class.getName(), ErrorCodeEnum.A0_410);

            /** ===========  A0_420("A0420","请求参数值超出允许的范围") =============*/

            /**
             * 带注释的元素大小必须在指定的边界(包括在内)之间。
             * 支持类型:
             * {@code CharSequence}(计算字符序列的长度)
             * {@code Collection}(计算集合大小)
             * {@code Map}(地图大小计算)
             * Array (计算数组长度)
             * {@code null}元素被认为有效。
             */
            put(Size.class.getName(), ErrorCodeEnum.A0_420);
            /**
             * 被注释的元素必须是一个数值，其值必须小于或等于等于指定的最大值。
             * 例如 @DecimalMax("10")          ->         <= 10
             * 支持的类型有:
             * {@code BigDecimal}
             * {@code BigInteger}
             * {@code CharSequence}
             * {@code byte}, {@code short}, {@code int}, {@code long}, 以及它们各自的包装
             * 注意，由于舍入错误，{@code double}和{@code float}不被支持
             * (一些提供者可能提供一些近似的支持)。
             * {@code null}元素认为是有效的。
             */
            put(DecimalMax.class.getName(), ErrorCodeEnum.A0_420);
            /**
             * 被注释的元素必须是一个数值，其值必须更高或等于指定的最小值。
             * 例如 @DecimalMin("5")          ->         >= 5
             * 支持的类型有:
             * {@code BigDecimal}
             * {@code BigInteger}
             * {@code CharSequence}
             * {@code byte}, {@code short}, {@code int}, {@code long}, 以及它们各自的包装
             * 注意，由于舍入错误，{@code double}和{@code float}不被支持
             * (一些提供者可能提供一些近似的支持)。
             * {@code null}元素认为是有效的。
             */
            put(DecimalMin.class.getName(), ErrorCodeEnum.A0_420);
            /**
             * 注释元素必须是可接受范围内的数字
             * 例如 @Digits(integer = 2, fraction = 2)    ->      99.99
             * 支持的类型有:
             * {@code BigDecimal}
             * {@code BigInteger}
             * {@code CharSequence}
             * {@code byte}， {@code short}， {@code int}， {@code long}，以及它们各自的包装类型
             * {@code null}元素认为是有效的。
             */
            put(Digits.class.getName(), ErrorCodeEnum.A0_420);
            /**
             * 被注释的元素必须是一个数值，其值必须小于或等于指定的最大值
             * 例如 @Max(10)          ->         <= 10
             * 支持的类型有:
             * {@code BigDecimal}
             * {@code BigInteger}
             * {@code byte}， {@code short}， {@code int}， {@code long}，以及它们各自的包装
             * 注意，由于舍入错误，{@code double}和{@code float}不被支持
             * (一些提供者可能提供一些近似的支持)。
             * {@code null}元素认为是有效的。
             */
            put(Max.class.getName(), ErrorCodeEnum.A0_420);
            /**
             * 被注释的元素必须是一个数值，其值必须更高或等于指定的最小值。
             * 例如 @Min(5)         ->         >= 5
             * 支持的类型有:
             * {@code BigDecimal}
             * {@code BigInteger}
             * {@code byte}， {@code short}， {@code int}， {@code long}，以及它们各自的包装
             * 注意，由于舍入错误，{@code double}和{@code float}不被支持
             * (一些提供者可能提供一些近似的支持)。
             * {@code null}元素认为是有效的。
             */
            put(Min.class.getName(), ErrorCodeEnum.A0_420);
            /**
             * 注释的元素必须是一个严格的负数(即0被认为是一个无效值)。
             * 支持的类型有:
             * {@code BigDecimal}
             * {@code BigInteger}
             * {@code byte}， {@code short}， {@code int}， {@code long}， {@code float}，
             * {@code double}和它们各自的包装器
             * {@code null}元素认为是有效的。
             */
            put(Negative.class.getName(), ErrorCodeEnum.A0_420);
            /**
             * 注释的元素必须是负数或0。 支持类型:
             * {@code BigDecimal}
             * {@code BigInteger}
             * {@code byte}， {@code short}， {@code int}， {@code long}， {@code float}，
             * {@code double}和它们各自的包装器
             * {@code null}元素被认为有效。
             */
            put(NegativeOrZero.class.getName(), ErrorCodeEnum.A0_420);
            /**
             * 注释的元素必须是严格的正数(即0被认为是无效值)。
             * 支持类型:
             * {@code BigDecimal}
             * {@code BigInteger}
             * {@code byte}， {@code short}， {@code int}， {@code long}， {@code float}，
             * {@code double}和它们各自的包装器
             * {@code null}元素被认为有效。
             */
            put(Positive.class.getName(), ErrorCodeEnum.A0_420);
            /**
             * 被注释的元素必须是正数或0。
             * 支持类型:
             * {@code BigDecimal}
             * 先导入BigInteger} {@code
             * {@code byte}， {@code short}， {@code int}， {@code long}， {@code float}，
             * {@code double}和它们各自的包装器
             * {@code null}元素被认为有效。
             */
            put(PositiveOrZero.class.getName(), ErrorCodeEnum.A0_420);
            /**
             * 注释元素必须是过去的某个瞬间、日期或时间。
             * 现在由附加到{@link Validator}或的{@link ClockProvider}定义
             * {@link ValidatorFactory}。 默认的{@code clockProvider}定义了当前时间
             * 根据虚拟机，如果需要，应用当前的默认时区。
             * 支持类型:
             * {@code java.util.Date}
             * {@code java.util.Calendar}
             * {@code java.time.Instant}
             * {@code java.time.LocalDate}
             * {@code java.time.LocalDateTime}
             * {@code java.time.LocalTime}
             * {@code java.time.MonthDay}
             * {@code java.time.OffsetDateTime}
             * {@code java.time.OffsetTime}
             * {@code java.time.Year}
             * {@code java.time.YearMonth}
             * {@code java.time.ZonedDateTime}
             * {@code java.time.chrono.HijrahDate}
             * {@code java.time.chrono.JapaneseDate}
             * {@code java.time.chrono.MinguoDate}
             * {@code java.time.chrono.ThaiBuddhistDate}
             * {@code null}元素被认为有效。
             */
            put(Past.class.getName(), ErrorCodeEnum.A0_420);
            /**
             * 被注释的元素必须是过去或现在的即时、日期或时间。
             * 现在由附加到{@link Validator}或的{@link ClockProvider}定义
             * {@link ValidatorFactory}。 默认的{@code clockProvider}定义了当前时间
             * 根据虚拟机，如果需要，应用当前的默认时区。
             * present的概念是相对于约束所依赖的类型来定义的
             * 使用。 例如，如果约束是在{@link Year}上，present就意味着整个
             * 今年。
             * 支持类型:
             * {@code java.util.Date}
             * {@code java.util.Calendar}
             * {@code java.time.Instant}
             * {@code java.time.LocalDate}
             * {@code java.time.LocalDateTime}
             * {@code java.time.LocalTime}
             * {@code java.time.MonthDay}
             * {@code java.time.OffsetDateTime}
             * {@code java.time.OffsetTime}
             * {@code java.time.Year}
             * {@code java.time.YearMonth}
             * {@code java.time.ZonedDateTime}
             * {@code java.time.chrono.HijrahDate}
             * {@code java.time.chrono.JapaneseDate}
             * {@code java.time.chrono.MinguoDate}
             * {@code java.time.chrono.ThaiBuddhistDate}
             * {@code null}元素被认为有效。
             */
            put(PastOrPresent.class.getName(), ErrorCodeEnum.A0_420);
            /**
             * 注释的元素必须是即时的、日期或未来的时间。
             * 现在由附加到{@link Validator}或的{@link ClockProvider}定义
             * {@link ValidatorFactory}。 默认的{@code clockProvider}定义了当前时间
             * 根据虚拟机，如果需要，应用当前的默认时区。
             * 支持类型:
             * {@code java.util.Date}
             * {@code java.util.Calendar}
             * {@code java.time.Instant}
             * {@code java.time.LocalDate}
             * {@code java.time.LocalDateTime}
             * {@code java.time.LocalTime}
             * {@code java.time.MonthDay}
             * {@code java.time.OffsetDateTime}
             * {@code java.time.OffsetTime}
             * {@code java.time.Year}
             * {@code java.time.YearMonth}
             * {@code java.time.ZonedDateTime}
             * {@code java.time.chrono.HijrahDate}
             * {@code java.time.chrono.JapaneseDate}
             * {@code java.time.chrono.MinguoDate}
             * {@code java.time.chrono.ThaiBuddhistDate}
             * {@code null}元素被认为有效。
             */
            put(Future.class.getName(), ErrorCodeEnum.A0_420);
            /**
             * 被注释的元素必须是当前或未来的即时、日期或时间。
             * 现在由附加到{@link Validator}或的{@link ClockProvider}定义
             * {@link ValidatorFactory}。 默认的{@code clockProvider}定义了当前时间
             * 根据虚拟机，如果需要，应用当前的默认时区。
             * 这里的present概念是相对于约束所处的类型定义的
             * 使用。 例如，如果约束是在{@link Year}上，present就意味着整个
             * 今年。
             * 支持类型:
             * {@code java.util.Date}
             * {@code java.util.Calendar}
             * {@code java.time.Instant}
             * {@code java.time.LocalDate}
             * {@code java.time.LocalDateTime}
             * {@code java.time.LocalTime}
             * {@code java.time.MonthDay}
             * {@code java.time.OffsetDateTime}
             * {@code java.time.OffsetTime}
             * {@code java.time.Year}
             * {@code java.time.YearMonth}
             * {@code java.time.ZonedDateTime}
             * {@code java.time.chrono.HijrahDate}
             * {@code java.time.chrono.JapaneseDate}
             * {@code java.time.chrono.MinguoDate}
             * {@code java.time.chrono.ThaiBuddhistDate}
             * {@code null}元素被认为有效。
             */
            put(FutureOrPresent.class.getName(), ErrorCodeEnum.A0_420);

            /** ===========  A0_153("A0153","邮箱格式校验失败") =============*/

            /**
             * 字符串必须是格式良好的电子邮件地址。 构成有效的确切语义
             * 电子邮件地址留给Bean验证提供者。 接受{@code CharSequence进行}。
             * PS:尽量别用这个，好好写正则去，判定太水 sui@163com 可以通过
             */
            put(Email.class.getName(), ErrorCodeEnum.A0_153);

        }
    };

    public static <T> void validate(T obj) {
        Set<ConstraintViolation<T>> violations = validator.validate(obj, Default.class);
        if (!violations.isEmpty()) {
//            log.info("[Param Validate Error] {}", violations);
            violations.stream().forEach(violation -> exception(violation));
        }
    }

    /**
     * 解析异常
     *
     * @param violation
     */
    public static void exception(ConstraintViolation violation) {
        ConstraintDescriptor<?> constraintDescriptor = violation.getConstraintDescriptor();
        /** 1. 优先获取已知的 javax.validation 注解标签，查看是否存在转换 */
        if (ANNOTATION_ENUM_MAPPING.containsKey(constraintDescriptor.getAnnotation().annotationType().getName())) {
            throw new BusinessException(ANNOTATION_ENUM_MAPPING.get(constraintDescriptor.getAnnotation().annotationType().getName()), append(violation), true);
        }
        /** 2. 解析是否是自定义异常，且属性值合法 */
        if (constraintDescriptor instanceof ConstraintDescriptorImpl descriptor) {
            if (AnnotatedElementUtils.hasAnnotation(descriptor.getAnnotationType(), ErrorCodeAttribute.class)) {
                if (descriptor.getAnnotationDescriptor().getAttribute(ErrorCodeConstants.ERROR_CODE_ENUM) instanceof ErrorCodeEnum errorCodeEnum) {
                    throw new BusinessException(errorCodeEnum, append(violation), true);
                }
            }
        }
        /** 3. 缺省错误码  A0_400("A0400","用户请求参数错误") 二级宏观错误码 */
        throw new BusinessException(ErrorCodeEnum.A0_400, append(violation), true);
    }

    /**
     * 补充 error message 信息
     *
     * @param violation
     * @return {@link String}
     */
    public static String append(ConstraintViolation violation) {
        ConstraintDescriptor<?> constraintDescriptor = violation.getConstraintDescriptor();
        if (constraintDescriptor instanceof ConstraintDescriptorImpl descriptor) {
            ElementType elementType = descriptor.getConstraintLocationKind().getElementType();
            /** 初始错误信息 */
            return switch (elementType) {
                case FIELD, METHOD -> append(elementType, violation);
                default -> violation.getMessage();
            };
        }
        return violation.getMessage();
    }

    /**
     * 优化打印格式
     *
     * @param elementType
     * @param violation
     * @return {@link String}
     */
    public static String append(ElementType elementType, ConstraintViolation violation) {
        JsonObject detail = new JsonObject();
        detail.addProperty(String.valueOf(elementType).toLowerCase(), String.valueOf(violation.getPropertyPath()));
        if (StringUtils.isNotBlank(violation.getMessage())) {
            detail.addProperty("details", violation.getMessage());
        }
        return detail.toString();
    }

}
